package calao;

import static calao.util.Bits.clearBits;
import static calao.util.Bits.hasBits;
import static calao.util.Logger.DEBUG;
import static calao.util.Logger.ERROR;
import static calao.util.Logger.INFO;
import static calao.util.Logger.bind;
import static calao.util.Logger.log;
import static calao.util.Logger.release;

import java.awt.Dimension;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import javax.servlet.Servlet;
import javax.swing.JButton;
import javax.swing.JFrame;

import org.mortbay.jetty.MimeTypes;
import org.mortbay.jetty.servlet.Context;
import org.mortbay.jetty.servlet.ServletHolder;

import calao.client.MemberService;
import calao.client.SessionService;
import calao.client.WorkspaceService;
import calao.storage.EqualCondition;
import calao.storage.Record;
import calao.storage.Search;
import calao.storage.Storage;
import calao.storage.bdb.BdbStorage;
import calao.util.Cursor;
import calao.util.FileHandler;
import calao.util.WeakMap;

/** Calao server.
 */
public class Server
{

	public static final String SERVER_ATTRIBUTE = "calao.server";
	
	static final int DEFAULT_PORT = 7080;

	static String defaultDesktop = "calao.Desktop";

	static {
		System.setProperty("org.mortbay.log.class", "calao.JettyLogger");
	}
	
	enum State { STARTING, RUNNING, STOPPING, STOPPED }; 

	private final WeakMap<Integer,Workspace> wsMap = 
		new WeakMap<Integer,Workspace>() {
		public Integer computeKey(Workspace ws) {
			return ws.id;
		}
	};

	private final Vector<Application> appList = new Vector<Application>();

	final ServerConfig config;
	final org.mortbay.jetty.Server webServer;
	final Context webContext;

	State state = State.STOPPED;
	Storage storage; // server storage
	Workspace rootWS;

	FileHandler fileLogHandler;

	/** Starts a new server.
	 */
	public static void main(String[] argv) 
	{
		ServerConfig config = new ServerConfig();
		for (int i=0; i<argv.length; i++) config.parseArg(argv[i], argv[++i]);
		final Server server = new Server(config);
		server.add(new DesktopServlet(), "/");
		server.start();	
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				server.stop();
			}
		});
		if (!config.demonize) {
			javax.swing.SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					server.launchAdminPanel();
				}
			});
		}
	}

	Server(ServerConfig config)
	{
		Calao.SCHEMA.register();
		this.config = config;
		if (config.port!=0) {
			webServer = new org.mortbay.jetty.Server(config.port);
			webContext = new Context(webServer, "/", Context.SESSIONS);
			webContext.setAttribute(SERVER_ATTRIBUTE, this);
		}
		else {
			webServer = null;
			webContext = null;
		}
	}

	public Server()
	{
		this(new ServerConfig());
	}

	public Server(int port)
	{
		this(new ServerConfig(port));
	}

	private void setupRoot()
	{
		int[] profiles = { 1 };
		rootWS.createMemberUser("admin", "admin", "Admin", profiles);
	}

	/** Starts this server.
	 */
	public synchronized void start() 
	{
		try {
			fileLogHandler = new FileHandler("log/server-%i.log");
			bind(fileLogHandler);
		}
		catch (Exception e) {
			log(e);
		}
		check(State.STOPPED);
		state = State.STARTING;
		storage = BdbStorage.open(0);
		rootWS = openWorkspace(1);
		if (rootWS==null) {
			createWorkspace(1, null, Workspace.ROOT_PATH);
			rootWS = openWorkspace(1);
			setupRoot();
		}
		startApplications();
		if (webServer!=null) {
			try {
				webServer.start();
				webContext.getServletContext().setAttribute("calao.server", this);
			}
			catch (Exception e) {
				if (e instanceof RuntimeException) throw (RuntimeException)e;
				throw new RuntimeException(e);
			}
		}
		state = State.RUNNING;
		log(INFO, "Server started on port %d", config.port);
	}

	/** Stops this server.
	 */
	public synchronized void stop()
	{
		if (state==State.STOPPED) return;
		check(State.RUNNING);
		state = State.STOPPING;
		if (webServer!=null) {
			try {
				webServer.stop();
			}
			catch (Exception e) {
				if (e instanceof RuntimeException) throw (RuntimeException)e;
				throw new RuntimeException(e);
			}
		}
		// first, wait for requests to terminate gracefully
		int rcount = WebRequest.getCount();
		for (int i=0; rcount>0 && i<5; i++) {
			log(INFO, "Still %d active request(s), waiting...", rcount);
			trySleeping(1000);
			rcount = WebRequest.getCount();
		}
		if (rcount>0) log(ERROR, "Still %d active request(s)", rcount);
		// second, close all sessions to release references to workspaces
		for (Workspace ws : wsMap.entries()) ws.closeSessions();
		rootWS = null;
		// now, wait for all workspaces to finalize
		forceCleanup();
		int wcount = Workspace.openWorkspaceCount;
		for (int i=0; wcount>0 && i<5; i++) {
			log(INFO, "Still %d opened workspace(s), waiting...", wcount);
			trySleeping(1000);
			forceCleanup();
			wcount = Workspace.openWorkspaceCount;
		}
		if (wcount>0) {
			log(ERROR, "Still %d opened workspace(s)", wcount);
			for (Workspace ws : wsMap.entries()) ws.forceClose();
			wsMap.clear(); 
		}
		stopApplications();
		storage.close();
		BdbStorage.closeEnvironment();
		state = State.STOPPED;
		log(INFO, "Server stopped");
		if (fileLogHandler!=null) {
			release(fileLogHandler);
			fileLogHandler.close();
		}
	}

	void trySleeping(long millis) 
	{
		try {
			Thread.sleep(millis);
		} 
		catch (InterruptedException e) {
			log(e);
		}		
	}

	void forceCleanup()
	{
		System.gc();
		System.gc();
	}

	/** Opens a session on a workspace.
	 * 
	 * @param path the path to the target workspace.
	 * @param alias the user alias.
	 * @param password the user password.
	 * @return A valid session if successful, null otherwise.
	 */
	public synchronized Session openSession(String path,
			String alias, String password)
	{
		check(State.RUNNING);
		int wsid = getWorkspaceId(path);
		if (wsid!=-1) {
			Workspace ws = getWorkspace(wsid);
			return ws.openSession(alias, password);			
		}
		return null;
	}

	public synchronized Session openSession(int wsid, String alias, String password)
	{
		Workspace ws = getWorkspace(wsid);
		if (ws==null) throw new IllegalArgumentException();
		return ws.openSession(alias, password);		
	}

	int getWorkspaceId(String path)
	{
		Search search = new Search(Workspace.TABLE);
		search.add(new EqualCondition(Workspace.PATH, path));
		search.limitTo(Workspace.ID, Workspace.PATH);
		Cursor<Record> cursor =	storage.listResults(search);
		try {
			return cursor.next()? cursor.get().get(Workspace.ID) : -1;
		}
		finally {
			cursor.close();
		}
	}

	String getWorkspacePath(int wsid)
	{
		Search search = new Search(Workspace.TABLE);
		search.add(new EqualCondition(Workspace.ID, wsid));
		search.limitTo(Workspace.ID, Workspace.PATH);
		Cursor<Record> cursor =	storage.listResults(search);
		try {
			return cursor.next()? cursor.get().get(Workspace.PATH) : null;
		}
		finally {
			cursor.close();
		}
	}

	/** Gets a workspace.
	 */
	public synchronized Workspace getWorkspace(int id)
	{
		Workspace ws = wsMap.get(id);
		if (ws==null) {
			ws = openWorkspace(id);
		}
		return ws;
	}

	private Workspace openWorkspace(int id)
	{
		Record wsRecord = storage.fetch(Workspace.TABLE, id);
		if (wsRecord!=null) {
			Integer pid = wsRecord.get(Workspace.PARENT_ID);
			Workspace parent = pid!=null? getWorkspace(pid) : null;
			Workspace ws = new Workspace(this, parent, id, wsRecord.get(Workspace.PATH),
					wsRecord.get(Workspace.PROPERTIES), BdbStorage.open(id));
			int flags = wsRecord.get(Workspace.FLAGS);
			boolean isNew = hasBits(flags, Workspace.NEW_FLAG);
			for (Application app : ws.getApplications()) app.onOpen(ws);
			ws.initServices();
			if (isNew) {
				setupNewWorkspace(ws);
				flags = clearBits(flags, Workspace.NEW_FLAG);
				wsRecord.set(Workspace.FLAGS, flags);
				storage.store(wsRecord);
			}
			wsMap.put(ws);					
			return ws;
		}
		return null;
	}

	private void setupNewWorkspace(Workspace ws)
	{
		ws.getProfileManager().createProfile("Admin", new String[] {
				SessionService.ADMIN.getName(),
				MemberService.ADMIN_MEMBERS.getName(),
				WorkspaceService.ADMIN_WORKSPACES.getName()
		});
	}

	public void launchAdminPanel() {
		//Create and set up the window.
		JFrame frame = new JFrame("Calao Server Admin Panel");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		final JButton stopButton = new JButton("Stop");
		stopButton.setPreferredSize(new Dimension(128, 32));
		frame.add(stopButton);
		stopButton.addActionListener(new java.awt.event.ActionListener() {
			public void actionPerformed(java.awt.event.ActionEvent evt) {
				stopButton.setEnabled(false);
				if (state==State.STOPPED) {
					start();
					stopButton.setText("Stop");
				}
				if (state==State.RUNNING) {
					stop();
					stopButton.setText("Start");
				}
				stopButton.setEnabled(true);
			}
		});
		//Display the window.
		frame.pack();
		frame.setVisible(true);
	}

	void saveProperties(Workspace ws, Map map) {
		Record record = storage.fetch(Workspace.TABLE, ws.id);
		record.set(Workspace.PROPERTIES, ws.properties.map);
		storage.store(record);
		log(DEBUG, "Saved workspace %d properties: %s", ws.id, ws.properties.map);
	}

	/**
	 * Creates a new workspace.
	 */
	void createWorkspace(int id, Integer pid, String path) 
	{
		// create entry in workspace table
		Record record = new Record(Workspace.TABLE);
		record.set(Workspace.ID, id);
		record.set(Workspace.PARENT_ID, pid);
		record.set(Workspace.PATH, path);
		record.set(Workspace.FLAGS, Workspace.NEW_FLAG);
		record.set(Workspace.PROPERTIES, new HashMap<String,String>());
		storage.store(record);
	}

	int newWorkspaceId()
	{
		return storage.getNextValue(Workspace.ID_SEQUENCE);
	}

	/** Adds a servlet to this server.
	 * @throws UnsupportedOperationException when this server does not listen 
	 * to web requests, i.e. was initialized with port set to 0.
	 */
	public void add(Servlet servlet, String path)
	{
		if (webContext==null) throw new UnsupportedOperationException();
		webContext.addServlet(new ServletHolder(servlet), path);	
	}

	private void check(State state)
	{
		if (this.state!=state) {
			throw new RuntimeException("server is "+this.state.name().toLowerCase());
		}
	}

	/** Gets the list of running applications.
	 */
	public Iterable<Application> getApplications()
	{
		return appList;
	}

	/** Gets an application running on this server.
	 */
	public Application getApplication(String name)
	{
		for (Application app : appList) if (app.name.equals(name)) return app;
		return null;
	}

	private void startApplications()
	{
		Application.Map map = new Application.Map();
		for (Application app : config.appList) map.put(app.name, app);
		for (Application app : config.appList) start(app, map);
	}
	
	private void start(Application app, Application.Map depMap)
	{
		for (String depName : app.dependencies) {
			Application dep = depMap.get(depName);
			if (dep==null) throw new DependencyException(depName);
			if (!appList.contains(dep)) start(dep, depMap);
		}
		appList.add(app);
		app.onStart(this);
		log(INFO, "Application %s started", app.name);
	}
	
	private void stopApplications()
	{
		while (appList.size()>0) {
			Application app = appList.lastElement();
			app.onStop(this);
			appList.setSize(appList.size()-1);
			log(INFO, "Application %s stopped", app.name);
		}
	}

	/** Gets MIME types recognized by this server when handling web requests.
	 * @throws UnsupportedOperationException when this server does not listen 
	 * to web requests, i.e. was initialized with port set to 0.
	 */
	public MimeTypes getMimeTypes()
	{
		if (webContext==null) throw new UnsupportedOperationException();
		return webContext.getMimeTypes();
	}

	/** Gets the root workspace.
	 */
	public Workspace getRootWorkspace()
	{
		return rootWS;
	}

	/** Deletes a workspace.
	 */
	public void deleteWorkspace(int wsid)
	{
		storage.drop(Workspace.TABLE, wsid);
	}
	
	public int getPort()
	{
		return config.port;
	}

}
